{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Composable Objects\n",
    "\n",
    "In this notebook, we show how you can combine multiple objects into a single top-level index.\n",
    "\n",
    "This approach works by setting up `IndexNode` objects, with an `obj` field that points to a:\n",
    "- query engine\n",
    "- retriever\n",
    "- query pipeline\n",
    "- another node!\n",
    "\n",
    "```python\n",
    "object = IndexNode(index_id=\"my_object\", obj=query_engine, text=\"some text about this object\")\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Data Setup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install llama-index-storage-docstore-mongodb\n",
    "%pip install llama-index-vector-stores-qdrant\n",
    "%pip install llama-index-storage-docstore-firestore\n",
    "%pip install llama-index-retrievers-bm25\n",
    "%pip install llama-index-storage-docstore-redis\n",
    "%pip install llama-index-storage-docstore-dynamodb\n",
    "%pip install llama-index-readers-file"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!wget --user-agent \"Mozilla\" \"https://arxiv.org/pdf/2307.09288.pdf\" -O \"./llama2.pdf\"\n",
    "!wget --user-agent \"Mozilla\" \"https://arxiv.org/pdf/1706.03762.pdf\" -O \"./attention.pdf\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from llama_index.core import download_loader\n",
    "\n",
    "from llama_index.readers.file import PyMuPDFReader\n",
    "\n",
    "llama2_docs = PyMuPDFReader().load_data(\n",
    "    file_path=\"./llama2.pdf\", metadata=True\n",
    ")\n",
    "attention_docs = PyMuPDFReader().load_data(\n",
    "    file_path=\"./attention.pdf\", metadata=True\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Retriever Setup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "os.environ[\"OPENAI_API_KEY\"] = \"sk-...\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from llama_index.core.node_parser import TokenTextSplitter\n",
    "\n",
    "nodes = TokenTextSplitter(\n",
    "    chunk_size=1024, chunk_overlap=128\n",
    ").get_nodes_from_documents(llama2_docs + attention_docs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from llama_index.core.storage.docstore import SimpleDocumentStore\n",
    "from llama_index.storage.docstore.redis import RedisDocumentStore\n",
    "from llama_index.storage.docstore.mongodb import MongoDocumentStore\n",
    "from llama_index.storage.docstore.firestore import FirestoreDocumentStore\n",
    "from llama_index.storage.docstore.dynamodb import DynamoDBDocumentStore\n",
    "\n",
    "docstore = SimpleDocumentStore()\n",
    "docstore.add_documents(nodes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from llama_index.core import VectorStoreIndex, StorageContext\n",
    "from llama_index.retrievers.bm25 import BM25Retriever\n",
    "from llama_index.vector_stores.qdrant import QdrantVectorStore\n",
    "from qdrant_client import QdrantClient\n",
    "\n",
    "client = QdrantClient(path=\"./qdrant_data\")\n",
    "vector_store = QdrantVectorStore(\"composable\", client=client)\n",
    "storage_context = StorageContext.from_defaults(vector_store=vector_store)\n",
    "\n",
    "index = VectorStoreIndex(nodes=nodes)\n",
    "vector_retriever = index.as_retriever(similarity_top_k=2)\n",
    "bm25_retriever = BM25Retriever.from_defaults(\n",
    "    docstore=docstore, similarity_top_k=2\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Composing Objects\n",
    "\n",
    "Here, we construct the `IndexNodes`. Note that the text is what is used to index the node by the top-level index.\n",
    "\n",
    "For a vector index, the text is embedded, for a keyword index, the text is used for keywords.\n",
    "\n",
    "In this example, the `SummaryIndex` is used, which does not technically need the text for retrieval, since it always retrieves all nodes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from llama_index.core.schema import IndexNode\n",
    "\n",
    "vector_obj = IndexNode(\n",
    "    index_id=\"vector\", obj=vector_retriever, text=\"Vector Retriever\"\n",
    ")\n",
    "bm25_obj = IndexNode(\n",
    "    index_id=\"bm25\", obj=bm25_retriever, text=\"BM25 Retriever\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from llama_index.core import SummaryIndex\n",
    "\n",
    "summary_index = SummaryIndex(objects=[vector_obj, bm25_obj])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Querying\n",
    "\n",
    "When we query, all objects will be retrieved and used to generate the nodes to get a final answer.\n",
    "\n",
    "Using `tree_summarize` with `aquery()` ensures concurrent execution and faster responses."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "query_engine = summary_index.as_query_engine(\n",
    "    response_mode=\"tree_summarize\", verbose=True\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[1;3;38;2;11;159;203mRetrieval entering vector: VectorIndexRetriever\n",
      "\u001b[0m\u001b[1;3;38;2;11;159;203mRetrieval entering bm25: BM25Retriever\n",
      "\u001b[0m"
     ]
    }
   ],
   "source": [
    "response = await query_engine.aquery(\n",
    "    \"How does attention work in transformers?\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Attention in transformers works by mapping a query and a set of key-value pairs to an output. The output is computed as a weighted sum of the values, where the weights are determined by the similarity between the query and the keys. In the transformer model, attention is used in three different ways: \n",
      "\n",
      "1. Encoder-decoder attention: The queries come from the previous decoder layer, and the memory keys and values come from the output of the encoder. This allows every position in the decoder to attend over all positions in the input sequence.\n",
      "\n",
      "2. Self-attention in the encoder: In a self-attention layer, all of the keys, values, and queries come from the same place, which is the output of the previous layer in the encoder. Each position in the encoder can attend to all positions in the previous layer of the encoder.\n",
      "\n",
      "3. Self-attention in the decoder: Similar to the encoder, self-attention layers in the decoder allow each position in the decoder to attend to all positions in the decoder up to and including that position. However, leftward information flow in the decoder is prevented to preserve the auto-regressive property.\n",
      "\n",
      "Overall, attention in transformers allows the model to jointly attend to information from different representation subspaces at different positions, improving the model's ability to capture dependencies and relationships between different parts of the input sequence.\n"
     ]
    }
   ],
   "source": [
    "print(str(response))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[1;3;38;2;11;159;203mRetrieval entering vector: VectorIndexRetriever\n",
      "\u001b[0m\u001b[1;3;38;2;11;159;203mRetrieval entering bm25: BM25Retriever\n",
      "\u001b[0m"
     ]
    }
   ],
   "source": [
    "response = await query_engine.aquery(\n",
    "    \"What is the architecture of Llama2 based on?\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The architecture of Llama 2 is based on the transformer model.\n"
     ]
    }
   ],
   "source": [
    "print(str(response))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[1;3;38;2;11;159;203mRetrieval entering vector: VectorIndexRetriever\n",
      "\u001b[0m\u001b[1;3;38;2;11;159;203mRetrieval entering bm25: BM25Retriever\n",
      "\u001b[0m"
     ]
    }
   ],
   "source": [
    "response = await query_engine.aquery(\n",
    "    \"What was used before attention in transformers?\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Recurrent neural networks, such as long short-term memory (LSTM) and gated recurrent neural networks, were commonly used before attention in transformers. These models were widely used in sequence modeling and transduction problems, including language modeling and machine translation.\n"
     ]
    }
   ],
   "source": [
    "print(str(response))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Note on Saving and Loading\n",
    "\n",
    "Since objects aren't technically serializable, when saving and loading, then need to be provided at load time as well.\n",
    "\n",
    "Here's an example of how I might save/load this setup."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Save"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# qdrant is already saved automatically!\n",
    "# we only need to save the docstore here\n",
    "\n",
    "# save our docstore nodes for bm25\n",
    "docstore.persist(\"./docstore.json\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Load"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from llama_index.core.storage.docstore import SimpleDocumentStore\n",
    "from llama_index.vector_stores.qdrant import QdrantVectorStore\n",
    "from qdrant_client import QdrantClient\n",
    "\n",
    "docstore = SimpleDocumentStore.from_persist_path(\"./docstore.json\")\n",
    "\n",
    "client = QdrantClient(path=\"./qdrant_data\")\n",
    "vector_store = QdrantVectorStore(\"composable\", client=client)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "index = VectorStoreIndex.from_vector_store(vector_store)\n",
    "vector_retriever = index.as_retriever(similarity_top_k=2)\n",
    "bm25_retriever = BM25Retriever.from_defaults(\n",
    "    docstore=docstore, similarity_top_k=2\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from llama_index.core.schema import IndexNode\n",
    "\n",
    "vector_obj = IndexNode(\n",
    "    index_id=\"vector\", obj=vector_retriever, text=\"Vector Retriever\"\n",
    ")\n",
    "bm25_obj = IndexNode(\n",
    "    index_id=\"bm25\", obj=bm25_retriever, text=\"BM25 Retriever\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# if we had added regular nodes to the summary index, we could save/load that as well\n",
    "# summary_index.persist(\"./summary_index.json\")\n",
    "# summary_index = load_index_from_storage(storage_context, objects=objects)\n",
    "\n",
    "from llama_index.core import SummaryIndex\n",
    "\n",
    "summary_index = SummaryIndex(objects=[vector_obj, bm25_obj])"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
